import {parse} from './parse';
import {round} from './helper';
import {invert} from './manipulate/invert';
import {ALPHA_PRECISION} from './constant';
import {rgbaToHex} from './color/model/hex';
import {roundRgba} from './color/model/rgb';
import {lighten} from './manipulate/lighten';
import {saturate} from './manipulate/saturate';
import {getBrightness} from './get/get-brightness';
import {changeAlpha} from './manipulate/change-alpha';
import {rgbaToRgbaText} from './color/model/rgb-text';
import {rgbaToHsla, roundHsla} from './color/model/hsl';
import {rgbaToHslaString} from './color/model/hsl-text';
import {rgbaToHsva, roundHsva} from './color/model/hsv';
import {AnyColor, HslaColor, HsvaColor, Input, RgbaColor} from './type';

export class Color {
    readonly rgba: RgbaColor;
    private readonly parsed: RgbaColor | null;

    constructor(input: AnyColor) {
        // Internal color format is RGBA object.
        // We do not round the internal RGBA numbers for better conversion accuracy.
        this.parsed = parse(input as Input)[0];
        this.rgba = this.parsed || {r: 0, g: 0, b: 0, a: 1};
    }

    /**
     * Returns a boolean indicating whether or not an input has been parsed successfully.
     * Note: If parsing is unsuccessful, Color defaults to black (does not throws an error).
     */
    public isValid(): boolean {
        return this.parsed !== null;
    }

    /**
     * Returns the brightness of a color (from 0 to 1).
     * The calculation logic is modified from WCAG.
     * https://www.w3.org/TR/AERT/#color-contrast
     */
    public brightness(): number {
        return round(getBrightness(this.rgba), 2);
    }

    /**
     * Same as calling `brightness() < 0.5`.
     */
    public isDark(): boolean {
        return getBrightness(this.rgba) < 0.5;
    }

    /**
     * Same as calling `brightness() >= 0.5`.
     * */
    public isLight(): boolean {
        return getBrightness(this.rgba) >= 0.5;
    }

    /**
     * Returns the hexadecimal representation of a color.
     * When the alpha channel value of the color is less than 1,
     * it outputs #rrggbbaa format instead of #rrggbb.
     */
    public toHex(): string {
        return rgbaToHex(this.rgba);
    }

    /**
     * Converts a color to RGB color space and returns an object.
     * Always includes an alpha value from 0 to 1.
     */
    public toRgb(): RgbaColor {
        return roundRgba(this.rgba);
    }

    /**
     * Converts a color to RGB color space and returns a string representation.
     * Outputs an alpha value only if it is less than 1.
     */
    public toRgbString(): string {
        return rgbaToRgbaText(this.rgba);
    }

    /**
     * Converts a color to HSL color space and returns an object.
     * Always includes an alpha value from 0 to 1.
     */
    public toHsl(): HslaColor {
        return roundHsla(rgbaToHsla(this.rgba));
    }

    /**
     * Converts a color to HSL color space and returns a string representation.
     * Always includes an alpha value from 0 to 1.
     */
    public toHslString(): string {
        return rgbaToHslaString(this.rgba);
    }

    /**
     * Converts a color to HSV color space and returns an object.
     * Always includes an alpha value from 0 to 1.
     */
    public toHsv(): HsvaColor {
        return roundHsva(rgbaToHsva(this.rgba));
    }

    /**
     * Creates a new instance containing an inverted (opposite) version of the color.
     */
    public invert(): Color {
        return color(invert(this.rgba));
    }

    /**
     * Increases the HSL saturation of a color by the given amount.
     */
    public saturate(amount = 0.1): Color {
        return color(saturate(this.rgba, amount));
    }

    /**
     * Decreases the HSL saturation of a color by the given amount.
     */
    public desaturate(amount = 0.1): Color {
        return color(saturate(this.rgba, -amount));
    }

    /**
     * Makes a gray color with the same lightness as a source color.
     */
    public grayscale(): Color {
        return color(saturate(this.rgba, -1));
    }

    /**
     * Increases the HSL lightness of a color by the given amount.
     */
    public lighten(amount = 0.1): Color {
        return color(lighten(this.rgba, amount));
    }

    /**
     * Increases the HSL lightness of a color by the given amount.
     */
    public darken(amount = 0.1): Color {
        return color(lighten(this.rgba, -amount));
    }

    /**
     * Changes the HSL hue of a color by the given amount.
     */
    public rotate(amount = 15): Color {
        return this.hue(this.hue() + amount);
    }

    /**
     * Allows to get or change an alpha channel value.
     */
    public alpha(): number;
    public alpha(value: number): Color;
    public alpha(value?: number): Color | number {
        if (typeof value === 'number') return color(changeAlpha(this.rgba, value));
        return round(this.rgba.a, ALPHA_PRECISION);
    }

    /**
     * Allows to get or change a hue value.
     */
    public hue(): number;
    public hue(value: number): Color;
    public hue(value?: number): Color | number {
        const hsla = rgbaToHsla(this.rgba);
        if (typeof value === 'number') return color({h: value, s: hsla.s, l: hsla.l, a: hsla.a});
        return round(hsla.h);
    }

    /**
     * Determines whether two values are the same color.
     */
    public isEqual(c: AnyColor | Color): boolean {
        return this.toHex() === color(c).toHex();
    }
}

/**
 * Parses the given input color and creates a new `Color` instance.
 * See accepted input formats: https://github.com/omgovich/colord#color-parsing
 */
export const color = (input: AnyColor | Color): Color => {
    if (input instanceof Color) return input;
    return new Color(input);
};
